RouteNameResolver.getName   B
last analyzed

Complexity

Conditions 5

Size

Total Lines 35
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 29
dl 0
loc 35
c 0
b 0
f 0
rs 8.7173
cc 5
1
// Inspired by: https://github.com/nestjs/nest/blob/cb2af8a3723272bcbacde44dcaadab66a7ec8b7c/packages/core/router/route-alias-resolver.ts
2
import { Injectable } from '@nestjs/common';
3
import { PATH_METADATA } from '@nestjs/common/constants';
4
import { isObject } from '@nestjs/common/utils/shared.utils';
5
import { DiscoveryService } from '@nestjs/core';
6
import { ROUTE_NAME_METADATA } from './WithName';
7
8
@Injectable()
9
export class RouteNameResolver {
10
  private readonly nameMap: Map<string, string[]>;
11
12
  constructor(discoveryService: DiscoveryService) {
13
    this.nameMap = new Map();
14
    this.init(discoveryService);
15
  }
16
17
  private init(discoveryService: DiscoveryService) {
18
    const controllers = discoveryService.getControllers();
19
20
    controllers.forEach(controller => {
21
      const basePath: string = Reflect.getMetadata(
22
        PATH_METADATA,
23
        controller.metatype
24
      );
25
26
      const methodNames = Object.getOwnPropertyNames(
27
        controller.instance.__proto__
28
      );
29
30
      methodNames.forEach(methodName => {
31
        const method = controller.instance.__proto__[methodName];
32
        const name: string | undefined = Reflect.getMetadata(
33
          ROUTE_NAME_METADATA,
34
          method
35
        );
36
37
        if (name) {
38
          const path: string = Reflect.getMetadata(PATH_METADATA, method);
39
          this.register(name, basePath, [path]);
40
        }
41
      });
42
    });
43
  }
44
45
  public register(name: string, basePath: string, path: string[]) {
46
    if (this.nameMap.has(name)) {
47
      throw new Error(`Conflict ${name} already registered`);
48
    }
49
    this.nameMap.set(name, this.createPath(basePath, path));
50
  }
51
52
  private resolveParamsInPart(part: string, params?: object): string {
53
    if (!isObject(params)) {
54
      return part;
55
    }
56
57
    // [':startDate', ':endDate']
58
    const paramNames = part.match(/:([a-zA-Z\d]+)/g);
59
60
    if (!paramNames) {
61
      return part;
62
    }
63
64
    let thePart = part;
65
66
    for (const paramName of paramNames) {
67
      const value = params[paramName.replace(':', '')];
68
69
      if (value) {
70
        thePart = thePart.replace(paramName, value);
71
      }
72
    }
73
74
    return thePart;
75
  }
76
77
  public resolve(name: string, params?: object): string {
78
    if (!this.nameMap.has(name)) {
79
      throw new Error(`Not Found: ${name} not registered`);
80
    }
81
82
    const path = this.nameMap.get(name).reduce((path, part) => {
83
      const thePart = this.resolveParamsInPart(part, params);
84
      return thePart ? path + '/' + thePart : path;
85
    }, '');
86
87
    return path ? path : '/';
88
  }
89
90
  public getName(path: string): string | null {
91
    const pathWithoutQueryString = path.split('?')[0];
92
93
    const parts = this.splitPath([pathWithoutQueryString]);
94
95
    const entries = Array.from(this.nameMap.entries());
96
    entries.sort(
97
      ([, patternA], [, patternB]) =>
98
        this.splitPath(patternB).length - this.splitPath(patternA).length
99
    );
100
101
    const matches = entries.filter(([, pattern]) => {
102
      const routeParts = this.splitPath(pattern);
103
104
      if (routeParts.length !== parts.length) {
105
        return false;
106
      }
107
108
      for (let index = 0; index < parts.length; index++) {
109
        const partLeft = parts[index];
110
        const partRight = routeParts[index];
111
        if (partLeft !== partRight && !partRight.startsWith(':')) {
112
          return false;
113
        }
114
      }
115
116
      return true;
117
    });
118
119
    if (matches.length === 0) {
120
      return null;
121
    }
122
123
    return matches[0][0];
124
  }
125
126
  private createPath(basePath: string, path: string[]): string[] {
127
    const base = basePath ? [this.stripSlashes(basePath)] : [];
128
    return base.concat(this.splitPath(path));
129
  }
130
131
  private splitPath(path: string[]): string[] {
132
    const pathParts = [];
133
    path.forEach(part => {
134
      part.split('/').forEach(partial => {
135
        partial && pathParts.push(this.stripSlashes(partial));
136
      });
137
    });
138
    return pathParts;
139
  }
140
141
  private stripSlashes(str: string) {
142
    return str.replace(/^\/?(.*)\/?$/, '$1');
143
  }
144
}
145